# Python 趣味编程：第 8 章
# 前半部分的总结(打破框架)
# --------------------------
# 程序名: ex08-blocks.py

from tkinter import Tk, Canvas, SW
from dataclasses import dataclass, field
import random
import time

# =================================================
# 初始设定值
BOX_MARGIN = 50
BOX_TOP_X = BOX_MARGIN # 游戏区域的左上X坐标
BOX_TOP_Y = BOX_MARGIN # 游戏区域的左上Y坐标
BOX_WIDTH = 700        # 游戏区域的宽度
BOX_HEIGHT = 500       # 游戏区域的高度
BOX_CENTER = BOX_TOP_X + BOX_WIDTH/2 # 游戏区域的中心

CANVAS_WIDTH = BOX_TOP_X + BOX_WIDTH + BOX_MARGIN    # Canvas的宽度
CANVAS_HEIGHT = BOX_TOP_Y + BOX_HEIGHT + BOX_MARGIN  # Canvas的高度
CANVAS_BACKGROUND = "lightgray"                      # Canvas的背景色

DURATION = 0.01        # 绘图间隔

NUM_COLS = 8                    # x方向的砖块数量
NUM_ROWS = 3                    # y方向的砖块数量
BLOCK_WIDTH = 80                # 砖块的宽度
BLOCK_HEIGHT = 30               # 砖块的高度
BLOCK_PAD = 5                   # 填充
BLOCK_TOP = 50                  # 砖块上的缝隙(绕进去用)
BLOCK_POINTS = 10               # 砖块的基础点
BLOCK_SPAN_X = BLOCK_WIDTH + BLOCK_PAD
BLOCK_SPAN_Y = BLOCK_HEIGHT + BLOCK_PAD
BLOCK_BOTTOM = BOX_TOP_Y + BLOCK_TOP + BLOCK_SPAN_Y * NUM_ROWS
BLOCK_COLORS = ["green", "blue", "darkgray"] #砖块的颜色

VX0 = [-3, -2, -1, 1, 2, 3]     # 球的x方向初速选项
BALL_DIAMETER = 10              # 球的直径
BALL_X0 = BOX_CENTER - BALL_DIAMETER/2      # 球的初始位置(×)
BALL_Y0 = BLOCK_BOTTOM + 10     # 球的初始位置(Y)
BALL_VX = random.choice(VX0)    # 球的x方向初速
BALL_VY = 3                     # 球的y方向初速
SPEED_UP = 10                   # 加速球的频率
BALL_MAX_VY = 8                 # 球的最高速度
MULTI_BALL_COUNT = 4            # 分裂球的频率
BALL_MAX_NUM = 5                # 分裂球的最大数量
BALL_COLOR = "red"              # 球的颜色

MESSAGE_Y = BALL_Y0 + 50        # 消息显示的Y坐标

PADDLE_WIDTH = 100              # 推杆的宽度
PADDLE_HEIGHT = 20              # 推杆的高度
PADDLE_X0 = BOX_TOP_X + (BOX_WIDTH - PADDLE_WIDTH)/2  # 推杆的初始位置(×)
PADDLE_Y0 = BOX_TOP_Y + BOX_HEIGHT - 60    # 推杆的初始位置(y)
PADDLE_VX = 5                   # 推杆的速度
PADDLE_COLORS = ["blue", "red", "green", "yellow", "brown", "gray"]
                                # 准备改变的颜色
PADDLE_SHORTEN = 10             # 每次缩短推杆的量
PADDLE_SHORTEN_COUNT = 5        # 缩短推杆的击打次数
PADDLE_MIN_W = 40               # 推杆的最小宽度 

SPEAR_WIDTH = 1                 # 枪的宽度
SPEAR_HEIGHT = 40               # 枪的高度
SPEAR_VY = 5                    # 枪掉落的速度
SPEAR_COLOR = "blue"            # 枪的颜色

CANDY_BONUS = 50                # 奖金点
CANDY_WIDTH = 10                # 奖励项目的宽度
CANDY_HEIGHT = 10               # 奖励项目的高度
CANDY_VY = 4                    # 奖励项目的掉落速度
CANDY_COLOR = "RED"             # 奖金项目的颜色

ADD_SCORE = 10                  # 得分的増加値

# ----------------------------------
# 定义MovingObject作为共同父类
@dataclass
class MovingObject:
    id: int
    x: int
    y: int
    w: int
    h: int
    vx: int
    vy: int

    def redraw(self):                   # 重新绘制(移动結果的画面反映)
        canvas.coords(self.id, self.x, self.y,
                      self.x + self.w, self.y + self.h)

    def move(self):                     # 使之移动
        self.x += self.vx
        self.y += self.vy


# Ball继承了MovingObject
class Ball(MovingObject):
    def __init__(self, id, x, y, d, vx, vy):
        MovingObject.__init__(self, id, x, y, d, d, vx, vy)
        self.d = d      # 作为直径记录


# Paddle继承了MovingObject
class Paddle(MovingObject):
    def __init__(self, id, x, y, w, h, c):
        MovingObject.__init__(self, id, x, y, w, h, 0, 0)
        self.c = c

    def set_v(self, v):
        self.vx = v     # #设定移动量的独特方法

    def stop(self):     # 停止也是Paddle独有的方法
        self.vx = 0


# Spear继承了MovingObject
class Spear(MovingObject):
    def __init__(self, id, x, y, w, h, vy, c):
        MovingObject.__init__(self, id, x, y, w, h, 0, vy)


class Candy(MovingObject):
    def __init__(self, id, x, y, w, h, vy, c):
        MovingObject.__init__(self, id, x, y, w, h, 0, vy)


# 砖块
@dataclass
class Block:
    id: int
    x: int
    y: int
    w: int
    h: int
    pt: int
    bc: int
    c: str


# ----------------------------------
# Box(游戏区域)的定义
@dataclass
class Box:
    west: int
    north: int
    east: int
    south: int
    balls: list
    paddle: Paddle
    paddle_v: int
    blocks: list
    duration: float
    run: int
    score: int
    paddle_count: int
    spear: Spear
    candy: Candy

    def __init__(self, x, y, w, h, duration):
        self.west, self.north = (x, y)
        self.east, self.south = (x + w, y + h)
        self.balls = []
        self.paddle = None
        self.blocks = []
        self.paddle_v = PADDLE_VX
        self.duration = duration
        self.run = False
        self.score = 0  # 得分
        self.paddle_count = 0    # 用推杆击杆的次数
        self.spear = None
        self.candy = None

    # 墙的生成
    def make_walls(self):
        canvas.create_rectangle(self.west, self.north, self.east, self.south,
                                outline="black")

    def create_ball(self, x, y, d, vx, vy):  # 球的生成
        id = canvas.create_oval(x, y, x + d, y + d, fill=BALL_COLOR)
        return Ball(id, x, y, d, vx, vy)

    # 推杆的生成
    def create_paddle(self, x, y, w, h, c):
        id = canvas.create_rectangle(x, y, x + w, y + h, fill=c)
        return Paddle(id, x, y, w, h, c)

    # 枪的生成
    def create_spear(self, x, y, w=SPEAR_WIDTH, h=SPEAR_HEIGHT, c=SPEAR_COLOR):
        id = canvas.create_rectangle(x, y, x + w, y + h, fill=c)
        return Spear(id, x, y, w, h, SPEAR_VY, c)

    # 奖金项目的生成
    def create_candy(self, x, y, w=CANDY_WIDTH, h=CANDY_HEIGHT, c=CANDY_COLOR):
        id = canvas.create_rectangle(x, y, x + w, y + h, fill=c)
        return Candy(id, x, y, w, h, CANDY_VY, c)

    def create_block(self, x, y, w, h, pt, bc, c):  # 砖块的生成
        id = canvas.create_rectangle(x, y, x + w, y + h, fill=c)
        return Block(id, x, y, w, h, pt, bc, c)

    def check_wall(self, ball):   # 碰壁时的处理
        if ball.y + ball.d + ball.vy >= self.south:  # 往下溜走了
            return True
        if (ball.x + ball.vx <= self.west \
            or ball.x + ball.d + ball.vx >= self.east):
            ball.vx = -ball.vx
        if ball.y + ball.vy <= self.north:
            ball.vy = -ball.vy
        return False

    def check_paddle(self, paddle, ball):  # 球击中推杆的处理
        hit = False
        # 从左打
        if (paddle.x <= ball.x + ball.d + ball.vx <= paddle.x + paddle.w
            and paddle.y <= ball.y + ball.d/2 + ball.vy <= paddle.y + paddle.h):
            hit = True
            ball.vx = - ball.vx
        # 从上打
        elif (paddle.y <= ball.y + ball.d + ball.vy <= paddle.y + paddle.h
            and paddle.x <= ball.x + ball.d/2 + ball.vx <= paddle.x + paddle.w):
            # 根据球的位置改变反弹角度
            hit = True
            ball.vx = int(6*(ball.x + ball.d/2 - paddle.x) / paddle.w) - 3
            ball.vy = - ball.vy
        # 从右打
        elif (paddle.x <= ball.x + ball.vx <= paddle.x + paddle.w \
            and paddle.y <= ball.y + ball.d/2 + ball.vy <= paddle.y + paddle.h):
            hit = True
            ball.vx = - ball.vx
        # 检查推杆的边界
        if paddle.x + paddle.vx <= self.west:
            paddle.stop()
            paddle.x = self.west
        elif self.east <= paddle.x + paddle.vx + paddle.w:
            paddle.stop()
            paddle.x = self.east - paddle.w
        if hit: # 球打在了推杆上
            self.paddle_count += 1
            if self.paddle_count % MULTI_BALL_COUNT == 0: # 产生球
                if len(self.balls) < BALL_MAX_NUM:
                    ball = self.create_ball(BALL_X0, BALL_Y0,
                                            BALL_DIAMETER,
                                            random.choice(VX0), BALL_VY)
                    self.balls.append(ball)
                    self.movingObjs.append(ball)
            if self.paddle_count % PADDLE_SHORTEN_COUNT == 0: # 要不要把推杆变短？
                if paddle.w > PADDLE_MIN_W:  # 还可以短一点!
                    paddle.w -= PADDLE_SHORTEN
            if self.paddle_count % SPEED_UP == 0: # 让球加速吗？
                if ball.vy > -BALL_MAX_VY: # 还能加速!
                    ball.vy -= 1   # 注意球是朝上的！

    def check_block(self, block, ball):  # 判定球是否击中推杆
        # 从上面打
        if (block.y <= ball.y + ball.d + ball.vy <= block.y + block.h \
            and block.x <= ball.x + ball.d/2 + ball.vx <= block.x + block.w):
            ball.vy = - ball.vy
            return True
        # 从右边打
        elif (block.x <= ball.x + ball.vx <= block.x + block.w
            and block.y <= ball.y + ball.d/2 + ball.vy <= block.y + block.h):
            ball.vx = - ball.vx
            return True
        # 从左边打
        elif (block.x <= ball.x + ball.d + ball.vx <= block.x + block.w
            and block.y <= ball.y + ball.d/2 + ball.vy <= block.y + block.h):
            ball.vx = - ball.vx
            return True
        # 从下边打
        elif (block.y <= ball.y + ball.vy <= block.y + block.h
            and block.x <= ball.x + ball.d/2 + ball.vx <= block.x + block.w):
            ball.vy = - ball.vy
            return True
        else:
            return False

    def check_spear(self, spear, paddle):
        if (paddle.x <= spear.x <= paddle.x + paddle.w \
            and spear.y + spear.h > paddle.y \
            and spear.y <= paddle.y + paddle.h):  # 中了枪
            return True
        else:
            return False

    def check_candy(self, candy, paddle):
        if (paddle.x <= candy.x <= paddle.x + paddle.w \
            and candy.y + candy.h > paddle.y \
            and candy.y <= paddle.y + paddle.h):  # 拿球
            return True
        else:
            return False

    def left_paddle(self, event):   # 将推杆向左移动(Event处理)
        self.paddle.set_v(- self.paddle_v)

    def right_paddle(self, event):  # 将推杆向右移动(Event处理)
        self.paddle.set_v(self.paddle_v)

    def stop_paddle(self, event):   # 停止转动(Event处理)
        self.paddle.stop()

    def game_start(self, event):
        self.run = True

    def game_end(self, message):
        self.run = False
        canvas.create_text(BOX_CENTER, MESSAGE_Y,
                           text=message, font=('FixedSys', 16))
        tk.update()

    def update_score(self):
        canvas.itemconfigure(self.id_score,
                             text="score:" + str(self.score))

    def wait_start(self):
        # 等待输入SPACE
        id_text = canvas.create_text(BOX_CENTER, MESSAGE_Y,
                                     text="Press 'SPACE' to start",
                                     font=('FixedSys', 16))
        tk.update()
        while not self.run:    # 一味地等待SPACE
            tk.update()
            time.sleep(self.duration)
        canvas.delete(id_text)  # 删除输入SPACE的信息
        tk.update()

    def set(self):   # 统一进行初始设定
        # 墙壁的绘制
        self.make_walls()
        # 分数的表示
        self.id_score = canvas.create_text(
            BOX_TOP_X,
            BOX_TOP_Y - 2,
            text=("score: " + str(self.score)),
            font=("FixedSys", 16), justify="left",
            anchor=SW
            )
        # 球的生成
        ball = self.create_ball(BALL_X0, BALL_Y0,
                                BALL_DIAMETER, BALL_VX, BALL_VY)
        self.balls.append(ball)
        # 推杆的生成
        self.paddle = self.create_paddle(PADDLE_X0, PADDLE_Y0,
                                         PADDLE_WIDTH, PADDLE_HEIGHT,
                                         random.choice(PADDLE_COLORS))
        # 砖块的生成
        for y in range(NUM_ROWS):
            bc = NUM_ROWS - y   # 砖块的硬度(1 ~ num_rows)
            for x in range(NUM_COLS):
                block = self.create_block(
                    self.west + x * BLOCK_SPAN_X + BLOCK_PAD,
                    self.north + y * BLOCK_SPAN_Y + BLOCK_TOP,
                    BLOCK_WIDTH, BLOCK_HEIGHT,
                    BLOCK_POINTS * bc, bc,
                    BLOCK_COLORS[bc - 1]
                    )
                self.blocks.append(block)
        # 事件处理的定义
        canvas.bind_all('<KeyPress-Right>', self.right_paddle)
        canvas.bind_all('<KeyPress-Left>', self.left_paddle)
        canvas.bind_all('<KeyRelease-Right>', self.stop_paddle)
        canvas.bind_all('<KeyRelease-Left>', self.stop_paddle)
        canvas.bind_all('<KeyPress-space>', self.game_start)  # SPACEが押された

    def animate(self):
        # 移动物品统一定义
        self.movingObjs = [self.paddle] + self.balls
        while self.run:
            for obj in self.movingObjs:
                obj.move()          # 移动坐标
            if self.spear:
                if self.check_spear(self.spear, self.paddle):
                    self.game_end("You are destroyed!")  # 游戏切换的显示
                    break               # 中了枪
            if self.candy:
                if self.check_candy(self.candy, self.paddle):
                    self.score += CANDY_BONUS
                    self.update_score()
                    canvas.delete(self.candy.id)
                    self.movingObjs.remove(self.candy)
                    self.candy = None
            for ball in self.balls:
                if self.check_wall(ball):  # 与墙壁的碰撞处理
                    canvas.delete(ball.id)
                    self.balls.remove(ball)
                    self.movingObjs.remove(ball)
                self.check_paddle(self.paddle, ball)  # 推杆反弹
                for block in self.blocks:
                    if self.check_block(block, ball): # 砖块碰撞
                        block.bc -= 1  # 硬度减少一个
                        if block.bc > 0:
                            canvas.itemconfigure(block.id,
                                                 fill=BLOCK_COLORS[block.bc - 1])
                        else:  # 硬度变成了零
                            self.score = self.score + block.pt
                            self.update_score()
                            canvas.delete(block.id)
                            self.blocks.remove(block)
            if len(self.balls) == 0:
                self.game_end("Game Over!")  # 游戏切换的显示
                break               # 把最后一个球漏在下面了
            if len(self.blocks) == 0:   # 如果我们去掉最后一块
                self.game_end("Clear!")  # 游戏切换的显示
                break                   # 脱落
            if self.spear==None and random.random() < 0.01:  # 1%的概率发生
                self.spear = self.create_spear(
                    random.randint(self.west, self.east),
                    self.north)
                self.movingObjs.append(self.spear)
            if self.candy==None and random.random() < 0.005:  # 0.5%的概率发生
                self.candy = self.create_candy(
                    random.randint(self.west, self.east),
                    self.north)
                self.movingObjs.append(self.candy)

            if self.spear and self.spear.y + self.spear.h >= self.south:
                canvas.delete(self.spear.id)
                self.movingObjs.remove(self.spear)
                self.spear = None
            if self.candy and self.candy.y + self.candy.h >= self.south:
                canvas.delete(self.candy.id)
                self.movingObjs.remove(self.candy)
                self.candy = None

            for obj in self.movingObjs:
                obj.redraw()    # 用移动后的坐标重新绘制(画面反映)
            time.sleep(self.duration)
            tk.update()

# -------------------
tk=Tk()
tk.title("Game")

canvas = Canvas(tk, width=CANVAS_WIDTH, height=CANVAS_HEIGHT, bg=CANVAS_BACKGROUND)
canvas.pack()

# ----------------------------------
# 主程序
box = Box(BOX_TOP_X, BOX_TOP_Y, BOX_WIDTH, BOX_HEIGHT, DURATION)
box.set()           # 游戏的初始设定
box.wait_start()    # 等待开始
box.animate()       # 动画
